Skip to content

feat(client): format one of webpack's problems, wherever it is read - #2438

Merged
alexander-akait merged 1 commit into
mainfrom
feat/client-problem-formatter
Sep 30, 2026
Merged

alexander-akait merged 1 commit into
mainfrom
feat/client-problem-formatter

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Stacked on #2437 — that branch is the base, so this shows only the delta.
GitHub retargets it to main when #2437 merges.

Why

webpack-dev-server has no overlay code left after this, which is the point.

The overlay took formatted strings only. The middleware formats its own
payloads on the server, so its client never needed anything else — but a
server that sends webpack's error objects to the browser and formats them
there had to write that formatting itself. That is what webpack-dev-server's
client-src/overlay.js was: 177 lines whose substance was
problemLocation/problemBody/problemLine (webpack's error object → the
overlay's string shape) and formatProblem for its console. An adapter is
still a client file.

What

showProblems takes an error object as it comes:

import { showProblems } from "webpack-dev-middleware/client/overlay";

showProblems("errors", stats.errors, "build");

and the formatting is a module of its own, for a console as much as an overlay:

import { formatProblem } from "webpack-dev-middleware/client/problem";

const { header, body } = formatProblem("error", error);
// { header: "ERROR in ./src/app.js 3:0", body: "Module parse failed: ..." }

problemLocation, problemBody, problemLine and formatProblem, exported
from ./client/problem and re-exported from ./client/overlay so a consumer
doing both has one import.

Two things the server got wrong

src/hot.js builds the same shape now, so a build reads the same way whether
the middleware formatted it or a server sent the object over. A unit test
asserts the two agree across every shape rather than trusting them to.

Falling out of that:

  • A heading with nothing in it. An error webpack names no module for sent
    " \nmessage" — a first line holding a single space — and the overlay reads
    the first line as the heading. The existing test pinned it
    (expect(formatErrors([{ message: "boom" }])).toEqual([" \nboom"])). It is
    reachable: a real build's Module not found: Error: Can't resolve 'x' for an
    entry comes through as { loc: "c", message: … } with no moduleName. The
    message now goes alone.
  • The loader chain instead of the file. A module built by loaders reports
    its whole request as moduleName, which reads as noise where the file is
    what matters. The file comes first now, the request after it, and file is
    used when webpack sets one — once, not appended to itself
    (./a.js (./a.js)), which is what both the naive version and my own first
    attempt produced. test/problem.test.js covers that case because it caught
    me.

I checked what webpack 5 actually sets before writing these rules, rather than
assuming: for a parse error, a missing module, and a loader failure,
moduleName is the bare module (./src/c.js) and file is unset — so the
loader and file branches are rules for input this project does not produce
itself, and matter for a server that sends its own objects.

Proof it does the job

On a dev-server checkout with this packed and installed,
client-src/overlay.js and test/client/ReactErrorBoundary.test.js (the React
error-boundary heuristic is tested here already) delete outright, and
client-src/index.js calls this package directly:

import configureOverlay, {
  clear as clearOverlay,
  formatProblem,
  showProblems,
} from "webpack-dev-middleware/client/overlay";

with the id, the Trusted Types policy name and the open-in-editor route as
configureOverlay options, showProblems/clearOverlay in place of the
send({ type }) state machine — the per-source slots are what that machine
was for — and formatProblem for the console. dev-server's overlay e2e suite:
39 pass. That change goes on webpack/webpack-dev-server#5750.

showProblems("warnings", filtered, source) is also called there without a
length check, which #2437 makes safe.

State

e2e 138 pass across 12 suites, unit 6909 across 17 (test/problem.test.js is
18 of them, formatErrors gained 5), lint / both typecheck passes / spelling /
the precompiled schema check / the full build clean. test/logging.test.js has
the two root-only chmod failures it has on main in this container.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • The error overlay can now display webpack error and warning objects alongside plain-text messages, including module, file, location, message, and stack details.
    • Added formatting utilities for presenting webpack problems outside the overlay.
  • Bug Fixes
    • Improved diagnostic formatting when module or file details are missing, and avoided repeating file information when it matches the module.

@changeset-bot

changeset-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 57f595d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-middleware Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

alexander-akait added a commit to webpack/webpack-dev-server that referenced this pull request Sep 29, 2026
`client-src/overlay.js` was the last of it: an adapter whose substance was
turning webpack's error objects into the shape the shared overlay renders, and
a `send({ type })` state machine over `showProblems`/`clear`. Both belong
elsewhere. The formatting is webpack-dev-middleware's `client/problem` now,
and the state machine was standing in for per-source slots that overlay
already has, so the events map onto two calls.

What stays is this package's identity on top of a shared overlay, as options:
the `webpack-dev-server-client-overlay` element id, so anything querying it is
unaffected; the `webpack-dev-server#overlay` Trusted Types policy name, which
a page's CSP allowlists by name; and the `/webpack-dev-server/open-editor`
route that makes a file reference clickable.

`test/client/ReactErrorBoundary.test.js` goes with it — the heuristic it
covers lives in webpack-dev-middleware, which tests it.

Requires webpack/webpack-dev-middleware#2438.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
Base automatically changed from fix/overlay-and-indicator-empty-states to main September 30, 2026 09:00
The overlay took formatted strings only. The middleware formats its own
payloads on the server, so its client never needed anything else — but a
server that sends webpack's error objects to the browser and formats them
there had to write that formatting itself, which is what webpack-dev-server
carried a client file for.

`showProblems` now takes an error object as it comes, and the formatting is
`webpack-dev-middleware/client/problem`: `problemLocation`, `problemBody`,
`problemLine` and `formatProblem`, the last splitting a problem into the
header and body a console wants.

The server's own `formatErrors` builds the same shape, so a build reads the
same way whether the middleware formatted it or a server sent the object over.
Two things it got wrong fall out of that:

  * An error webpack names no module for sent a first line holding a single
    space. The overlay reads the first line as the heading, so it drew one
    with nothing in it. The message goes alone now.
  * A module built by loaders reports its whole request as `moduleName`
    (`babel-loader!./app.js`), which reads as noise where the file is what
    matters. The file comes first, the request follows it, and `file` is used
    when webpack sets one — once, not appended to itself.

A unit test asserts the two formatters agree on every shape rather than
trusting them to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@alexander-akait
alexander-akait force-pushed the feat/client-problem-formatter branch from 1e47daa to 57f595d Compare September 30, 2026 09:04
@coderabbitai

coderabbitai Bot commented Sep 30, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 1ed82d43-3f2c-43cc-87a9-09c27054f909

📥 Commits

Reviewing files that changed from the base of the PR and between d54e87d and 57f595d.

📒 Files selected for processing (10)
  • .changeset/feat-problem-formatter.md
  • client-src/overlay.js
  • client-src/problem.js
  • package.json
  • src/hot.js
  • test/e2e/overlay.test.js
  • test/hot.test.js
  • test/problem.test.js
  • types/client/overlay.d.ts
  • types/client/problem.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.


Walkthrough

The change adds browser-side formatters for webpack problems and exposes them through the package and overlay declarations. The overlay now accepts strings or problem objects. Server error headings include available module, loader, file, and location details, and omit an empty heading when those details are absent. Tests cover client formatting, overlay rendering, and server output.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 57f59

The change adds structured problem formatting while preserving supported server inputs. No merge-blocking issue remains; merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 57f59

The change primarily broadens diagnostic input and formatting. Structured problems are converted into strings before display, and no new privilege or boundary bypass was demonstrated. Risk remains low rather than minimal because HTML escaping and failure recovery could not be fully verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is diagnostic text reaching the browser overlay, plus downstream consumers of the new formatting API. Repository evidence does not establish how external consumers render those strings or what environments expose the overlay.

Trust Boundaries and Controls

  • observed — Structured problem fields are normalized into strings before reaching the overlay rendering path. Inspected rendering code calls encodeHtmlEntity before HTML generation and assignment. Helper correctness and the exact base comparison remain unverified, so this supports continuity of the display boundary without proving complete injection resistance.

Resilience and Maintainability Implications

  • inferred — Available evidence places normalization before per-source storage and preserves empty-input clearing, which argues against partial per-item state mutation during normalization. It does not resolve whether formatter failures, deferred iframe creation, or interleaved calls can leave stale visible diagnostics; no material recovery regression was established.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: client-side formatting of webpack problems across their consumers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 8 files. (2 skipped: 2 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.22%. Comparing base (d54e87d) to head (57f595d).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2438      +/-   ##
==========================================
+ Coverage   96.15%   96.22%   +0.06%     
==========================================
  Files          19       20       +1     
  Lines        2161     2200      +39     
==========================================
+ Hits         2078     2117      +39     
  Misses         83       83              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@alexander-akait
alexander-akait merged commit cc0b244 into main Sep 30, 2026
22 checks passed
@alexander-akait
alexander-akait deleted the feat/client-problem-formatter branch September 30, 2026 10:38
alexander-akait added a commit to webpack/webpack-dev-server that referenced this pull request Oct 5, 2026
`client-src/overlay.js` was the last of it: an adapter whose substance was
turning webpack's error objects into the shape the shared overlay renders, and
a `send({ type })` state machine over `showProblems`/`clear`. Both belong
elsewhere. The formatting is webpack-dev-middleware's `client/problem` now,
and the state machine was standing in for per-source slots that overlay
already has, so the events map onto two calls.

What stays is this package's identity on top of a shared overlay, as options:
the `webpack-dev-server-client-overlay` element id, so anything querying it is
unaffected; the `webpack-dev-server#overlay` Trusted Types policy name, which
a page's CSP allowlists by name; and the `/webpack-dev-server/open-editor`
route that makes a file reference clickable.

`test/client/ReactErrorBoundary.test.js` goes with it — the heuristic it
covers lives in webpack-dev-middleware, which tests it.

Requires webpack/webpack-dev-middleware#2438.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
alexander-akait added a commit to webpack/webpack-dev-server that referenced this pull request Oct 17, 2026
… here (#5750)

* refactor(client): take the WebSocket transport from dev-middleware

First step of moving the hot clients into webpack-dev-middleware: this
package's `WebSocketClient` is a strict subset of the one that package now
ships, and worse in three ways. It has no `close()` at all, despite
declaring `@implements {CommunicationClient}`, which the interface requires.
It has no guard against an event the socket had already queued reporting
after the caller closed, so a close could schedule a reconnection nobody
asked for. And it hands the url to `new WebSocket` unresolved, which throws
on browsers before Chrome 125 / Firefox 124 / Safari 17.3 for a relative or
`http(s):` url — this package always builds an absolute `ws:` url, so that
one never bit here, but it is a trap for anyone reusing the class.

Re-exported rather than deleted: `client.webSocketTransport` resolves to
this path, so anything pointing at it keeps working.

`import/no-unresolved` is switched off for the file, following the exemption
already in place for `@changesets/get-github-info`: the import resolver
cannot follow an `exports` subpath. TypeScript does resolve it, so
`lint:types-client` still covers the import.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* refactor(client): take the error overlay from dev-middleware too

692 lines of overlay deleted in favour of the one dev-middleware ships —
the state machine, the iframe, the rendering, the runtime-error listeners.
What is left is an adapter: this package's `createOverlay`/`send` shape on
top of that overlay's `showProblems`/`clear`, plus `formatProblem`, which
stays because the console uses it and because this package still receives
webpack's error objects rather than formatted strings.

Three things had to be carried across so nothing outward changes:

  * the element id. It is what a test, a screenshot tool or an integration
    finds the overlay by, so `webpack-dev-server-client-overlay` is passed
    through dev-middleware's new `overlay.id` rather than renamed;
  * the Trusted Types policy name. Under an enforced
    `require-trusted-types-for 'script'` the page's CSP allowlists a policy
    by name, and dev-middleware's default is a different one, so
    `webpack-dev-server#overlay` is passed explicitly — including where
    this package's option is `false`, which means "no name of my own";
  * `/webpack-dev-server/open-editor`. dev-middleware leaves the endpoint
    empty by default, having no route to point at, so the file references
    in a problem would have stopped being clickable.

The first two came from the e2e suite failing, not from reading the code,
which is the argument for swapping under the tests rather than after.

What does change is the overlay's internal DOM, which is not an interface
this package documents: a clickable file reference is `[data-open-file]`
rather than `[data-can-open]`, and a problem is headed by its level and
origin rather than "Compiled with problems". Two tests assert on those and
move with the implementation.

Checked against a baseline taken first: the overlay suite fails exactly
the set it fails on a clean `main` in this container — no case newly
failing, none newly passing — and the client and web-socket-url suites are
unmoved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* feat(client): add Server-Sent Events as a second transport

`client.webSocketTransport: "sse"` connects the page with `EventSource`
instead of a WebSocket. The endpoint is webpack-dev-middleware's hot endpoint,
which is an ordinary response rather than an upgrade, so there is no second
server to start and it sits in the middleware chain where everything this
package puts in front of it can still see it.

Naming it as the client's transport picks the endpoint that serves it, so
`webSocketServer` does not have to be set as well; `webSocketServer: "sse"` is
accepted too. `lib/servers/EventSourceServer.js` is the adapter between that
endpoint and the transport shape the rest of this package is written against,
so nothing above the wire can tell which one it is talking to — same protocol,
same messages, same options.

All of the protection stays here: the endpoint hands over the request each
client connected with, and `allowedHosts` and the origin check decide as they
do for a socket. One difference in how that check reads a request. A browser
puts an `Origin` on a WebSocket handshake whether or not it is cross-origin,
so one without it is refused; `EventSource` sends no `Origin` on a same-origin
request, so for a stream an absent one is taken as a page of this server's own.
A stream that does carry an `Origin` — which is what a cross-origin page sends
— is checked as before, and the `Host` check runs either way.

Both transports are the middleware's now, re-exported from
`client-src/clients/`, and each has a test of its own: the contract both
answer, plus the silence watchdog only a stream needs. `test/e2e/event-source.test.js`
drives a real browser through a hot update over the stream, the handshake it
is greeted with, and both sides of the host check.

`socket.js` no longer throws on a frame it cannot parse. A stream's keep-alive
has to be a `data:` frame rather than a comment, or the client's own watchdog
would count a quiet connection as a dead one, so something that is not JSON
now arrives on the wire by design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* refactor(client): no overlay code left to adapt

`client-src/overlay.js` was the last of it: an adapter whose substance was
turning webpack's error objects into the shape the shared overlay renders, and
a `send({ type })` state machine over `showProblems`/`clear`. Both belong
elsewhere. The formatting is webpack-dev-middleware's `client/problem` now,
and the state machine was standing in for per-source slots that overlay
already has, so the events map onto two calls.

What stays is this package's identity on top of a shared overlay, as options:
the `webpack-dev-server-client-overlay` element id, so anything querying it is
unaffected; the `webpack-dev-server#overlay` Trusted Types policy name, which
a page's CSP allowlists by name; and the `/webpack-dev-server/open-editor`
route that makes a file reference clickable.

`test/client/ReactErrorBoundary.test.js` goes with it — the heuristic it
covers lives in webpack-dev-middleware, which tests it.

Requires webpack/webpack-dev-middleware#2438.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* feat: reuse webpack-dev-middleware's browser runtime

This server has no client of its own any more. `client-src/` is gone — the
runtime, the error overlay, the progress indicator, the logger, the socket
wrapper and the WebSocket client, around two thousand lines that existed in
near-duplicate in both projects — and the middleware's hot option drives all
of it, including putting the entry and `HotModuleReplacementPlugin` into the
compilation.

Nothing about a configuration changes. Every option keeps its name and its
meaning and is mapped onto the middleware's vocabulary in `lib/hotOptions.js`:
`hot`/`liveReload` become one `apply` mode, `client.reconnect` becomes
`connect.retries`, `client.webSocketURL` is already the shape the middleware's
`hot.client.path` takes, and the overlay is passed through with this project's
own element id so anything querying for it still finds it.

The `webpack-dev-server/client/*` paths are generated re-exports of the
middleware's client, so importing them resolves to what it always did. The
paths that were only this bundle's internals are no longer published.

Protection stays here. `allowedHosts`, the `Origin` checks and the same-origin
rule are unchanged: the middleware is handed `cors: true` and no token, which
is it applying no policy of its own, and every connection is judged here
before anything is published to it. A handshake is refused before it completes
rather than after, since the middleware exposes the upgrade rather than taking
the server over.

A `webSocketServer` of your own still works. A class, a module exporting one,
or options giving the socket a port or a server of its own are wrapped into
the shape the middleware asks a custom transport for, so there is one path
from a build to a page either way, and `BaseServer` keeps its export.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* refactor: serve "sse" from the middleware's own transport

The adapter this branch added, `lib/servers/EventSourceServer.js`, existed to
dress the middleware's hot endpoint up as the transport shape this server was
written against. There is no such shape any more — the middleware owns both
ends of both transports — so naming `sse` is now just choosing one of its
transports, and the adapter and its special case in `normalizeOptions` are
gone with it.

`client.webSocketTransport: "sse"` and `webSocketServer: "sse"` keep working
and keep picking each other, which is what the option values are for. The
published `client/clients/EventSourceClient.js` path is generated alongside
its WebSocket neighbour, so a transport named by module still resolves.

One thing this caught: `"sse"` was being read as an implementation of this
server's own, since the test for that was "not `ws`" — which sent it looking
for a class to construct and failed before a page could connect. Both of the
middleware's transports are named now, and a smoke run over each confirms the
endpoint answers: `426` to a plain GET on the WebSocket one, an event stream
on the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* fix: keep this package's name and its refusals in the browser console

Two things the browser used to say that the middleware's runtime had no way
to say for it.

The console prefix: every message read `[webpack-dev-server]`, and four
snapshots record exactly that, including `Invalid Host/Origin header` — a line
people search for. Taking the runtime as-is would relabel all of it with a
dependency's name and send anyone with a problem to the wrong repository, so
the name travels with the logging option.

The refusal itself: a client turned away by `allowedHosts` or the origin check
is told why, and the reason is logged in the page. That needs the handshake to
complete, so the upgrade is answered even for a client that will be refused
and `guardHotEndpoint` closes it immediately after saying so — which is what
this server has always done. Owning the `upgrade` listener is still what keeps
a proxied upgrade with whoever it belongs to; `isHotEndpoint` went with the
earlier attempt to refuse before the handshake, which would have made the
reason unreachable.

Verified in a browser: the console carries `[webpack-dev-server]`, and a
client from a disallowed origin receives
`{"action":"error","message":"Invalid Host/Origin header"}` and is closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: wait for a build rather than the handshake's configuration push

Two proxy cases waited for `{ type: "hot" }`, which was this server telling a
client what to enable right after the handshake. That configuration is in the
client's entry query now, so the first thing the socket says is the build the
page is to run.

What the cases are about is untouched and still asserted: the HMR upgrade must
not reach the proxy target (`backendUpgradeCount` stays `0`), and must not
make the proxy log an error. Both hold.

`test/server/open-option.js` fails 33/33 here and on `main` alike — it drives
a real browser through `open`, which this environment has none of.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* fix: refuse an event-stream client, not just a WebSocket one

`allowedHosts` and the origin check were not being enforced on the `sse`
transport. The guard ran and decided to refuse, and then neither half of the
refusal landed: the message went through `sendMessage`, which tests
`readyState === 1` and calls `client.send` — a `ServerResponse` has neither,
so the loop skipped it — and `client.close()`, which a `ServerResponse` also
does not have, threw and was swallowed. The stream stayed open and kept
receiving builds.

The message goes through the middleware's `publishTo` now, which puts it on
the wire the client is actually on, and the connection is dropped the way that
client is dropped: a WebSocket closes, a response ends.

Verified on both transports: a disallowed origin is told
`Invalid Host/Origin header` and dropped, over a WebSocket and over a stream
alike — and a same-origin page, which `EventSource` gives no `Origin` at all,
is still let through.

Found via a CodeRabbit review of the middleware's README example, which had
the same two mistakes in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* fix: treat an empty `webSocketURL` part as one that was not given

A `client.webSocketURL` written as a string is normalized by parsing it, which
fills in every part — as `""` for the ones the url did not carry. Those were
passed straight through to the middleware, which refuses an empty port: so
`webSocketURL: "ws://example.test/ws"` stopped the server from starting at
all.

Empty has always meant "not said" here, and both the old client and the new
one resolve such a part from the page, so it is left out rather than sent.

The `allowed-hosts` snapshots are regenerated with it. Three changes, all
consequences of the runtime moving rather than of anything going wrong:
`connected` is logged where this server used to log which features were
enabled, `[HMR] Waiting for update signal from WDS...` is gone with the
`webpack/hot/dev-server` entry that printed it, and the prefix is still
`[webpack-dev-server]`. The suite is 31 of 32 here; the one failure is an
IPv6 case, and this container cannot listen on `::1` at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* fix: drop a test of a file that no longer exists, and regenerate the types

Two things CI's Lint job reports that are not about the dependency.

`test/client/clients/EventSourceClient.test.js` was added earlier on this
branch and imports `client-src/clients/EventSourceClient.js`, which is gone.
It never survived the rebase as a deletion because it did not exist on the
side that deleted the rest of `test/client/`. Every case in it tests the
middleware's own stream client through a re-export, and the middleware's
`test/client-transports.test.js` already covers all six — plus the ones only a
stream has — so nothing is lost.

`types/` is tracked and the Lint job fails with "Missing types" when a build
changes it. `Server.d.ts` still described the removed methods, and the two
new modules had no declarations at all.

The changeset now says the two public methods that went with them,
`getClientEntry()` and `getClientHotEntry()`, are a breaking removal. It
recorded the rest of the removal and not that.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: regenerate the web-socket-server-url console snapshots

The suite reads the browser's console, which is now the middleware's client
talking. Every difference is one the migration makes on purpose:

  - the startup banner, `Server started: Hot Module Replacement enabled, …`,
    and `[HMR] Waiting for update signal from WDS...` are gone — the old client
    printed the first from the configuration this server pushed after the
    handshake, and the second came from the `webpack/hot/dev-server` entry this
    server no longer adds;
  - `Hey.` comes first, because the page no longer waits on a banner;
  - the reconnect cases gain `Trying to reconnect...` and `Disconnected!`.

Regenerated only after reading what kind of failure each of the 34 was: all
snapshot mismatches, none of them an error.

One test here was worse than a mismatch. "should not work and output disconnect
wrong web socket URL" waits for `Disconnected!` with no timeout, and the
middleware's client did not print it, so it spun forever with its server and
browser open and took the whole file to the runner's 400 s limit. That is fixed
in webpack-dev-middleware, which now says so when the connection goes away;
with it the file runs in 53 s and passes twice running.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* fix: keep manual client entries, page-url opt-outs and a hand-made middleware working

- `client/index.js` is now CommonJS and leaves the query written on it where
  the middleware's client reads it, so `client/index.js?...` entries keep
  their options instead of connecting with defaults.
- Name the client's page-url parameters after this package again, so
  `?webpack-dev-server-hot=false` and `?webpack-dev-server-live-reload=false`
  still opt a tab out.
- `client: false` no longer throws while reading `client.webSocketTransport`.
- A middleware mounted through `setupMiddlewares` has no hot endpoint of this
  server's, so there is nothing to guard.
- Update the e2e expectations for the middleware's client: its messages, the
  order of `connected` after the page's own output, and a wait that no longer
  polls forever for a message the client does not log.
- Drop the tests of the removed `getClientEntry()`/`getClientHotEntry()`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: bring the end-to-end suites in line with the middleware's client

Every difference is one the migration makes on purpose, and the ones that
were not are fixed:

- Provide the transport through `__webpack_dev_server_client__` again for
  `ws` and `sse` too, as the global has always held the one in use, and have
  the `client/` re-exports hand back the default export of the modules that
  have one.
- Point the overlay at this server's open-editor route.
- Expect what the middleware's client logs, with the build's timing and
  absolute paths normalized, and its message for each way an update goes.
- Port the tests that reached into the removed WebSocket server: the
  heartbeat, the client count, the host check on the event stream, and the
  federation entries, which are now checked on a browser target — a build for
  node is given no client.
- Replace the tests of the old progress element with ones of what
  `client.progress` promises, the indicator's own being the middleware's.
- Add unit tests for the options this server hands the middleware.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: update the command line tests for the middleware's client, and say what differs

The hot option is now visible in the client's entry rather than in a
webpack/hot entry, and a build for node is given neither. The changeset lists
what a page and its console look like now, which it had called nothing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: record what a CI browser sees, not what a sandbox does

Snapshots regenerated in a sandbox carried its differences with them:

- The overlay's style attributes came out with border: none, where the
  browser CI uses writes the four longhands the committed snapshots have.
- A sandbox proxy refuses connections to the addresses the web socket URL
  tests use with a 403, so those entries recorded a refusal where CI connects
  or fails to resolve the name.
- The IPv6 entry of the allowed hosts test could not run there, so its
  snapshot was dropped by the update.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: write the changed file's path the same on every platform

A rebuild's message names the file that changed, and a Windows machine writes
that path with backslashes, so the snapshots that record it could only match
on a machine that does not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: wait for the client to give up on a name that never resolves

How long a machine takes to say a name does not resolve is its own, and the
page was idle before the answer came on one runner, so the snapshot was taken
of "Hey." alone. The test now waits for the message that ends the sequence it
records.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: wait for the worker's client to connect before the snapshot

The worker's own client connects after the page has settled, so on a
slow runner the snapshot could see one "connected" line instead of two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* chore: merge this change's changesets into one entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* feat: leave the last of the compiler and socket wiring to the middleware

dev-server no longer applies a ProvidePlugin or HotModuleReplacementPlugin,
or runs a WebSocket server of its own: `client: false`, a custom
`client.webSocketTransport`, `webSocketServer.options` (a port or server
of its own, verifyClient, compression) and the HMR plugin for every
compilation are webpack-dev-middleware's `hot` options now.
lib/servers/WebsocketServer.js and bridge.js are gone; BaseServer stays
as the contract for a `webSocketServer` implementation of your own.

And what a page or a tool written against 6.0 relies on keeps working:

- `client/socket.js` and this server's own `{ type }` messages, which
  React Refresh's overlay reads off the socket
- `webSocketServer.clients` and `.implementation`, `sendMessage()`
  (deprecated; `static-changed` reloads), `getClientEntry()` and
  `getClientHotEntry()` (deprecated)
- the Trusted Types policy `webpack-dev-server#overlay`, progress off
  unless asked, a one second ping, overlay filters handed `{ message }`
- a hand-written `client/index.js` entry connects to `/ws` as before
- a `setupMiddlewares` that replaces the dev middleware (a Hono app)
  keeps the hot endpoint
- the editor is opened on the file resolved against webpack's context

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* fix: keep a subclass's client entries, the client paths in use and universal builds working

- `getClientEntry()` and `getClientHotEntry()` overridden by a subclass
  put their modules in `entry` again, the client one with the query this
  server always wrote, and the middleware then adds no client of its own.
- `client/` keeps only the paths something outside this package uses:
  `index.js`, written into `entry` by hand; `socket.js`, which React
  Refresh reads; and `clients/WebSocketClient.js`, which a transport of
  someone else's extends, still with `default` for `require()`.
- `client/index.js` hands its query over only where there is a `self`,
  so a universal build runs in Node without it throwing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: main's suite on the middleware's client, and React Refresh's hot scenarios

main's tests run as they are wherever they still pass; the rest only
record what the middleware's client says and shows, in its console,
overlay and progress indicator. A subclass's `getClientEntry()` and
`getClientHotEntry()` keep main's test.

React Refresh is tested with `@pmmmwh/react-refresh-webpack-plugin`
itself: a component keeps its state across one edit and several, a
child module updates in place, changed hooks remount in place, its
overlay shows a build error and an error thrown while rendering and
clears once fixed, an unaccepted edit reloads, `hot: "only"` does not,
and all of it over Server-Sent Events too. A universal build is served
to a page and run in Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* chore(ci): TEMPORARY - install the unreleased webpack-dev-middleware

DO NOT MERGE THIS COMMIT. Revert it, and set the dependency back to a
released `^8.4.0`, once webpack-dev-middleware ships.

This branch needs what the published 8.3.0 does not have: `hot.protocol`
with this server's runtime in `client/webpack-dev-server/`, `onConnect`
with the request, `handleUpgrade`, `publishTo`, `hot.ws` and the `ws`
server handed out as `hot.implementation`. CI installs from the
registry, so without this `tsc` fails in the Build step and every job
behind it is cancelled.

This vendors a pack of webpack-dev-middleware `main` plus
webpack/webpack-dev-middleware#2472 (feat/finish-dev-server-migration).
The lockfile change is confined to that one dependency.

To undo, with the release out:

    git revert <this commit>
    npm install webpack-dev-middleware@^8.4.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* refactor: follow the middleware's hot.client.url and pageParamPrefix names

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* chore(ci): TEMPORARY - re-vendor webpack-dev-middleware with the settled option names

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: check the hot option against webpack-dev-middleware's schema

Builds the middleware from each shape of configuration, so a name renamed
on either side fails here, and checks a hand-written client entry still
honours this server's page-url parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* refactor: hand a custom client to hot.client.module, and turn off both halves together

Follows webpack-dev-middleware's split: `client.webSocketTransport` naming
a module now maps onto `hot.client.module`, with `transport` left as the
wire; and "add nothing" is `client: false` plus `inject: false`, since
`inject: false` alone now only leaves the plugin out. Re-vendors the
middleware build carrying the change (TEMPORARY until it is released).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* refactor: map client: false onto the middleware's hot.inject enum

Reverts the `hot.client.module` mapping, so `client.webSocketTransport`
goes to `hot.client.transport` again; `client: false` becomes
`inject: "plugin"` with `hot` and `inject: "none"` without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* chore(ci): TEMPORARY - re-vendor webpack-dev-middleware with the hot.inject enum

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* chore: mark sendStats and the client/socket shim for the next major release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* chore: regenerate types for the sendStats TODO

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* chore(ci): TEMPORARY - re-vendor webpack-dev-middleware main (f084ec9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* chore(deps): depend on webpack-dev-middleware ^8.4.0

Replaces the TEMPORARY vendored build with the released package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

* test: buffer split SSE frames and accept extra connects in the worker test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant